package com.gotokeep.keep.composition.demo.internal;

import android.graphics.SurfaceTexture;
import android.media.AudioTrack;
import android.media.MediaCodec;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.media.MediaMetadataRetriever;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import android.opengl.GLUtils;
import android.os.Build;
import android.os.SystemClock;
import android.util.Log;
import android.view.Surface;

import com.gotokeep.keep.composition.demo.renderer.Renderer;

import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLContext;
import javax.microedition.khronos.egl.EGLDisplay;
import javax.microedition.khronos.egl.EGLSurface;
import javax.microedition.khronos.opengles.GL;

/**
 * @author xana/cuixianming
 * @version 1.0
 * @since 2018-04-28 09:56
 */
public class SpeedRenderThread extends Thread implements SurfaceTexture.OnFrameAvailableListener {
    private static final String TAG = SpeedRenderThread.class.getSimpleName();
    private AtomicBoolean shouldRender;

    private SurfaceTexture inputSurfaceTexture;
    private Surface inputSurface;
    private int inputTexId;

    private SurfaceTexture outputSurfaceTexture;

    private MediaExtractor mediaExtractor;
    private MediaFormat videoFormat;
    private MediaCodec videoDecoder;
    private MediaCodec.BufferInfo videoInfo = new MediaCodec.BufferInfo();

    private Renderer renderer;

    private EGL10 mEgl;
    private EGLDisplay mEglDisplay = EGL10.EGL_NO_DISPLAY;
    private EGLContext mEglContext = EGL10.EGL_NO_CONTEXT;
    private EGLSurface mEglSurface = EGL10.EGL_NO_SURFACE;
    private EGLConfig[] configs;
    private GL mGL;

    private int width = 0;
    private int height = 0;
    private int rotation = 0;
    private int frameRate = 0;
    private int keyFrameInterval = 0;
    private String filePath = "";
    private int videoTrackIndex = -1;

    private RenderListener renderListener;

    private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
    private static final int EGL_OPENGL_ES2_BIT = 4;
    private boolean isVideoEnd = false;
    private float speed = 1.0f;
    private boolean firstFrameNoticed = false;

    public SpeedRenderThread(String filePath, SurfaceTexture outputSurfaceTexture, Renderer renderer,
                             AtomicBoolean shouldRender, float speed) {
        this.outputSurfaceTexture = outputSurfaceTexture;
        this.renderer = renderer;
        this.shouldRender = shouldRender;
        this.filePath = filePath;
        this.speed = speed;
    }

    public Surface createInputSurface() {
        if (inputSurfaceTexture == null) {
            int texId[] = new int[1];
            GLES20.glGenTextures(1, texId, 0);
            GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId[0]);

            GLES20.glTexImage2D(GLES11Ext.GL_TEXTURE_BINDING_EXTERNAL_OES, 0, GLES20.GL_RGBA,
                    width, height, 0, GLES20.GL_RGBA,
                    GLES20.GL_UNSIGNED_BYTE, null);
            GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER,
                    GLES20.GL_LINEAR);
            GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER,
                    GLES20.GL_LINEAR);

            GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S,
                    GLES20.GL_REPEAT);
            GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T,
                    GLES20.GL_REPEAT);

            inputTexId = texId[0];
            inputSurfaceTexture = new SurfaceTexture(inputTexId);
            inputSurfaceTexture.setOnFrameAvailableListener(this);
            inputSurface = new Surface(inputSurfaceTexture);
//            int[] surfaceAttribs = {
//                    EGL10.EGL_WIDTH, width,
//                    EGL10.EGL_HEIGHT, height,
//                    EGL10.EGL_NONE
//            };
//            mInputEglSurface = mEgl.eglCreatePbufferSurface(mEglDisplay, configs[0], surfaceAttribs);
        }
        return inputSurface;
    }

    private void initDecoder(String filePath) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
            throw new RuntimeException("系统版本需要 4.3 以上");
        }
        File mediaFile = new File(filePath);
        if (!mediaFile.exists()) {
            throw new RuntimeException("文件不存在");
        }
        mediaExtractor = new MediaExtractor();
        try {
            mediaExtractor.setDataSource(filePath);
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }

        videoTrackIndex = -1;
        for (int i = 0; i < mediaExtractor.getTrackCount(); i++) {
            MediaFormat format = mediaExtractor.getTrackFormat(i);
            String mime = format.getString(MediaFormat.KEY_MIME);
            if (mime.startsWith("video/")) {
                videoTrackIndex = i;
                videoFormat = format;
                break;
            }
        }

        mediaExtractor.selectTrack(videoTrackIndex);
        width = videoFormat.getInteger(MediaFormat.KEY_WIDTH);
        height = videoFormat.getInteger(MediaFormat.KEY_HEIGHT);
        rotation = getRotation(filePath);
        if (renderListener != null) {
            renderListener.onVideoSizeChanged(width, height, rotation);
        }
        rotation = getRotation(filePath);
        frameRate = videoFormat.getInteger(MediaFormat.KEY_FRAME_RATE);
        frameRate = (frameRate == 0) ? 24 : frameRate;
//        keyFrameInterval = videoFormat.getInteger(MediaFormat.KEY_I_FRAME_INTERVAL);

        String mime = videoFormat.getString(MediaFormat.KEY_MIME);
        try {
            videoDecoder = MediaCodec.createDecoderByType(mime);
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }
    }

    private void initGL() {
        mEgl = (EGL10) EGLContext.getEGL();

        mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
        if (mEglDisplay == EGL10.EGL_NO_DISPLAY) {
            throw new RuntimeException("eglGetdisplay failed : " +
                    GLUtils.getEGLErrorString(mEgl.eglGetError()));
        }

        int[] version = new int[2];
        if (!mEgl.eglInitialize(mEglDisplay, version)) {
            throw new RuntimeException("eglInitialize failed : " +
                    GLUtils.getEGLErrorString(mEgl.eglGetError()));
        }

        int[] configAttribs = {
                EGL10.EGL_BUFFER_SIZE, 32,
                EGL10.EGL_ALPHA_SIZE, 8,
                EGL10.EGL_BLUE_SIZE, 8,
                EGL10.EGL_GREEN_SIZE, 8,
                EGL10.EGL_RED_SIZE, 8,
                EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
                EGL10.EGL_SURFACE_TYPE, EGL10.EGL_WINDOW_BIT,
                EGL10.EGL_NONE
        };

        int[] numConfigs = new int[1];
        configs = new EGLConfig[1];
        if (!mEgl.eglChooseConfig(mEglDisplay, configAttribs, configs, 1, numConfigs)) {
            throw new RuntimeException("eglChooseConfig failed : " +
                    GLUtils.getEGLErrorString(mEgl.eglGetError()));
        }

        int[] contextAttribs = {
                EGL_CONTEXT_CLIENT_VERSION, 2,
                EGL10.EGL_NONE
        };
        mEglContext = mEgl.eglCreateContext(mEglDisplay, configs[0], EGL10.EGL_NO_CONTEXT, contextAttribs);
        mEglSurface = mEgl.eglCreateWindowSurface(mEglDisplay, configs[0], outputSurfaceTexture, null);
        if (mEglSurface == EGL10.EGL_NO_SURFACE || mEglContext == EGL10.EGL_NO_CONTEXT) {
            int error = mEgl.eglGetError();
            if (error == EGL10.EGL_BAD_NATIVE_WINDOW) {
                throw new RuntimeException("eglCreateWindowSurface returned  EGL_BAD_NATIVE_WINDOW. ");
            }
            throw new RuntimeException("eglCreateWindowSurface failed : " +
                    GLUtils.getEGLErrorString(mEgl.eglGetError()));
        }

        if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) {
            throw new RuntimeException("eglMakeCurrent failed : " +
                    GLUtils.getEGLErrorString(mEgl.eglGetError()));
        }

        mGL = mEglContext.getGL();
    }

    private void init(String filePath) {
        initGL();
        initDecoder(filePath);
        createInputSurface();
        videoDecoder.configure(videoFormat, inputSurface, null, 0);
        videoDecoder.start();
//        audioDecoder.configure(audioFormat, null, null, 0);
//        audioDecoder.start();
//        audioTrack.play();
    }

    private void destroyGL() {
        if (inputTexId >= 0) {
            int []texID = new int[1];
            texID[0] = inputTexId;
            GLES20.glDeleteTextures(1, texID, 0);
        }

        mEgl.eglDestroyContext(mEglDisplay, mEglContext);
        mEgl.eglDestroySurface(mEglDisplay, mEglSurface);
        mEglContext = EGL10.EGL_NO_CONTEXT;
        mEglSurface = EGL10.EGL_NO_SURFACE;
    }

    private void destroyDecoder() {
        videoDecoder.stop();
        videoDecoder.release();
        videoDecoder = null;

//        audioDecoder.stop();
//        audioDecoder.release();
//        audioDecoder = null;
//
//        audioTrack.stop();
//        audioTrack.release();
//        audioTrack = null;

        mediaExtractor.release();
        mediaExtractor = null;
    }

    private void release() {
        destroyDecoder();
        if (inputSurface != null) {
            inputSurface.release();
        }
        inputSurface = null;
        inputSurfaceTexture = null;

        destroyGL();
    }

    private int getRotation(String path) {
        MediaMetadataRetriever retriever = new MediaMetadataRetriever();
        try {
            retriever.setDataSource(path);
            String rotation = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
            return Integer.parseInt(rotation);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return 0;
    }

    private boolean generateFrame(long playTime) {
        boolean decoded = false;
        while (!decoded && !isVideoEnd) {
            int inputIndex = videoDecoder.dequeueInputBuffer(1000);
            if (inputIndex >= 0) {
                ByteBuffer buffer = getInputBuffer(videoDecoder, inputIndex);
                buffer.clear();
                mediaExtractor.selectTrack(videoTrackIndex);
                int ret = mediaExtractor.readSampleData(buffer, 0);
                if (ret < 0) {
                    videoDecoder.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                    isVideoEnd = true;
                } else {
                    long videoTimestamp = mediaExtractor.getSampleTime();
                    videoDecoder.queueInputBuffer(inputIndex, 0, ret, videoTimestamp, mediaExtractor.getSampleFlags());
                    mediaExtractor.advance();
                }
            }
            int outputIndex = videoDecoder.dequeueOutputBuffer(videoInfo, 1000);
            switch (outputIndex) {
                case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
                    videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
                    break;
                case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
                    videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
                    break;
                case MediaCodec.INFO_TRY_AGAIN_LATER:
                    break;
                default:
                    videoDecoder.releaseOutputBuffer(outputIndex, true);
                    decoded = true;
                    break;
            }
        }
        if (!firstFrameNoticed && renderListener != null) {
            renderListener.onFirstFrameRendered();
            firstFrameNoticed = true;
        }
        return decoded && !isVideoEnd;
    }

    private ByteBuffer getInputBuffer(MediaCodec decoder, int inputIndex) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            return decoder.getInputBuffer(inputIndex);
        } else {
            return decoder.getInputBuffers()[inputIndex];
        }
    }

    private ByteBuffer getOutputBuffer(MediaCodec decoder, int outputIndex) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            return decoder.getOutputBuffer(outputIndex);
        } else {
            return decoder.getOutputBuffers()[outputIndex];
        }
    }

    @Override
    public void run() {
        init(filePath);
        if (renderer != null) {
            renderer.onInit(inputTexId);
        }

        long startMs = SystemClock.elapsedRealtime();
        long lastTime = startMs;
        isVideoEnd = false;
        videoInfo.presentationTimeUs = 0;
        while (shouldRender != null && shouldRender.get() != false) {
            lastTime = SystemClock.elapsedRealtime();
            long playTime = lastTime - startMs;
            boolean rendered = true;
//            if (keyFrameInterval != 0 && keyFrameInterval * speed)
            while (videoInfo.presentationTimeUs / 1000 <= playTime * speed) {
                rendered &= generateFrame((long) (playTime * speed));
            }
            if (rendered && renderer != null) {
                if (renderListener != null) {
                    renderListener.onFrameTime(playTime);
                }
                inputSurfaceTexture.updateTexImage();
                renderer.onDrawFrame();
                Log.d(TAG, "Render frame");
                mEgl.eglSwapBuffers(mEglDisplay, mEglSurface);
            } else {
                break;
            }
        }
        if (renderListener != null) {
            renderListener.onVideoEnd();
        }

        Log.d(TAG, "Render completed.");

        if (renderer != null) {
            renderer.onRelease();
        }

        release();
    }

    @Override
    public void onFrameAvailable(SurfaceTexture surfaceTexture) {
        Log.d(TAG, "onFrameAvailable: ");
    }

    public RenderListener getRenderListener() {
        return renderListener;
    }

    public void setRenderListener(RenderListener renderListener) {
        this.renderListener = renderListener;
    }

    public interface RenderListener {
        void onVideoSizeChanged(int width, int height, int rotation);
        void onVideoEnd();
        void onFrameTime(long timeUs);
        void onFirstFrameRendered();
    }
}
